#ifndef FEMDRAWER_H
#define FEMDRAWER_H

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <iostream>
#include <cctype>

/*
Windows include MUST be before GL includes, otherwise
the compiler generates the wrong calling convention (sigh)
*/

#if defined(_WIN32)
#include <windows.h>
#endif

#include <gl/GL.h>
#include <gl/glut.h>

#include "SweepDescription.h"
#include "CutPerformer.h"
#include "FEMModel.h"
#include "FEMDrawer.h"
#include "Triangle.h"
#include "node.h"
#include "Types.h"
#include "Matrix.h"
#include "MISC.h"
#include "SurfaceSelector.h"
#include "Tetrahedron.h"
#include "Scenario.h"
#include "ModelIO.h"
#include "Statistics.h"
#include "DeformationState.h"
#include "Setting.h"
#include "Trackball.h"

/*
where will the next cut be?

PRE: draw->cut_performer_ != NULL
*/

float fixed_col[] = {0,1,1,1};
float selected_col [] = {0,1,0};
float background [] = {1,1.2f,1};
float background_strength = 0.6f;
int max_mflops;			// ugh.

const Real INCR = 5.0;
const Real FACTOR = 0.001;
void DisplayTetrahedronQuality (FEMDrawer *drawer);

FEMDrawer::FEMDrawer (FiniteElementModel*fem)
{
	fem_ = fem;
	stereo_direction_ = (stereo_global_b) ? 1 : 0;

	max_mflops = (int) GetNumberSetting ("maximum-mflops");
	cut_performer_ =0;

	marked_ = 0;
	marked_node_ = 0;  

	distance_ = 2.;
	intersect_dist_ = 0.0;


	/*

	To ensure that the mouse gives nice feedback, the forces have to
	be scaled. Here we scale them to gravity forces and tissue
	response (Young modulus).  Gravity is proportional to density and
	element volume (= element-diameter^3)

	*/

	force_factor_ = GetNumberSetting ("young") * 1e-2
		* (GetNumberSetting ("gravity")  >0.0? GetNumberSetting ("gravity"):1.0)
		* GetNumberSetting ("density")
		* pow (MinimumElementLength (fem_), 3) ;

	if (!strcmp (GetStringSetting ("elasticity"), "neohooke"))
		force_factor_ *= 10; 

	velocity_ = 0.0;

	// not HW accelerated on G450.
	edge_b_ = false;		

	fog_b_ = true;
	relaxation_b_ = true;
	surface_selector_b_ = false;
	normal_b_ = false;
	inside_b_ = false;
	original_b_ = false;
	show_back_b_ = false;
	force_display_b_ = true;
	tet_quality_b_ = false;
	smooth_b_ = false;

	// HW accelerated on G450
	solid_b_ = true;
	lighting_b_ = true;

	selection_ray_.origin_ = Vector3 (0, 0, -2);
	selection_ray_.dir_ = Vector3 (0,0,1);

	Real  m = 0;
	for (int i=0; i<3; i++)
		m = m>background[i]?m:background[i];
	if (m)
	{
		for (int i=0; i<3; i++)
			background[i] *= float(background_strength / m);
	}

	Trackball(view_drag_.quat_, 0.0f, 0.0f, -0.3f, -0.1f );
}


DragState::DragState()
{
	Trackball(quat_, 0.0, 0.0, 0.0, 0.0 );
}

void FEMDrawer::FocusOnModel ()
{
	Vector3 c;
	Real rad ;
	NodesBoundingSphere (fem_, true, &c,&rad);

	distance_ = (rad * 10) >2? (rad * 10):2; 
	/*
	view_drag_.translate_ = c;
	view_drag_.translate_(2) = 0.0;
	*/

	Real  fov = 3.5 * atan (rad  / distance_) * 180.0 / M_PI ; 
	view_drag_.scale_(0) = log (fov) / log (2.0);
}

void FEMDrawer::SetMarked (TopologicalObject *o, Node * n)
{
	if (o)
	{
		marked_node_ = 0 ;
		marked_ = o;
	}
	else if (n)
	{
		marked_ =0;
		marked_node_ =n;
	}
	else
	{
		marked_ = 0;
		marked_node_ =0;
	}
}

void FEMDrawer::SetFrustrum ()
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity ();

	Real fov =  pow (2, view_drag_.scale_(0)); 
	gluPerspective(fov, 1.0, +1, 50);
	SetCamera ();
}

void FEMDrawer::DrawScalpel ()
{
	if (cut_performer_)
	{
		cut_performer_->Visualize (this);
		this_sweep_.Visualize (this); 
		last_sweep_.Visualize (this);
	}
}

void FEMDrawer::SetCamera (int direction)
{
	/* view */
	glMatrixMode(GL_MODELVIEW);

	/* transformations */
	GLfloat m[4][4];
	glLoadIdentity();

	Real eyedist = stereo_direction_ * 0.5 * GetNumberSetting ("eye-distance");
	glTranslated (eyedist, 0, - distance_);

	Real phi = atan(eyedist / distance_);
	glRotated (phi, 0,1,0);

	BuildRotMatrix( m, view_drag_.quat_ );
	glMultMatrixf( &m[0][0] );

	Vector3 space_trans = view_drag_.translate_ * 5;

	glTranslated (space_trans(0),
		space_trans (1),
		space_trans (2));
}  

/*
Add some force.
*/
void FEMDrawer::AdjustForce (Vector3 dir)
{
	if (marked_)
	{
		for (int i=0; i < marked_->GetSet ().Count (); i++)
		{
			fem_->AddForce (marked_->GetNode (i),  dir);
		}

		// deformation invalidates the distance. 
		intersect_dist_ = 0;
	}
}

void FEMDrawer::DrawFeature ()
{
	SetColor (selected_col);

	if (marked_ && marked_->Valid ())
	{
		if (Triangle * t = dynamic_cast<Triangle*> (marked_))
			DrawTriangle (t, true);
		else if (Tetrahedron* t = dynamic_cast<Tetrahedron*> (marked_))
			for (int i =0; i < 4; i++)
				DrawTriangle (t->GetTriangle (i), true);
	}
	else if (marked_node_ && marked_node_->Valid ())
	{
		glPointSize (5.0);
		glBegin (GL_POINTS);
		EnterVertex (marked_node_->Location (true));
		glEnd ();
	}
}


void FEMDrawer::FixNode (Node * n , bool fix)
{
	fem_->FixNode (n, fix);
}

void FEMDrawer::Setup ()
{
	glClear (GL_COLOR_BUFFER_BIT);
	glDisable (GL_LINE_SMOOTH);
	glEnable (GL_BLEND);
	glBlendFunc (GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
	glClearColor (background[0],background[1],background[2],background[3]);
	glFlush();
	FocusOnModel ();
	SetCamera ();
	SetFrustrum ();
	SetDepthCue ();
	SetLighting ();

	UpdateFaces ();
}

void FEMDrawer::DrawTriangle (Triangle* t, bool solid)
{
	if (solid)
		glBegin (GL_TRIANGLES);
	else
		glBegin (GL_LINE_LOOP);

	Vector3 v[3];
	Vector3 n[3];
	bool normals = true;

	for (int i=0; i < 3; i++)
	{
		Node * nod = t->GetNode (i);
		v[i] = nod->Location (true);

		if (!smooth_b_ ||  !vertex_counts_[nod->Number ()] )
			normals = false;
		else
			n[i] = surface_normals_[nod->Number ()];
	}

	int idx[3]= {0,1,2};

	if (t->GetSet ().Parity ())
	{
		idx[0] = 1;
		idx[1] = 0;
	}

	if (lighting_b_ && solid && !smooth_b_)
	{
		Vector3 n (t->Normal (true));
		n /= n.Length ();
		glNormal3dv (n.elts_);
	}

	for (int j = 0; j < 3; j++)
	{
		if (normals)
		{
			glNormal3dv (n[idx[j]].elts_);
		}
		EnterVertex (v[idx[j]]);
	}
	glEnd ();


	/*
	draw normals.
	*/
	if (normal_b_)
	{
		Vector3 c = Centroid (t->GetSet (), true);
		glBegin (GL_LINES);
		EnterVertex (c);
		c  += t->Normal (true);
		EnterVertex (c);
		glEnd ();
	}

}

void FEMDrawer::EnterVertex (Vector3 v)
{
	glVertex3dv (v.elts_);
}

/*
todo: should color fixed & deformed points.
*/
void FEMDrawer::Draw ()
{

	if (relaxation_b_ && relaxation_global_b)
	{
		long long start_count = flop_count;
		long long  last_count = -1;

		/*
		should calibrate.
		*/
		long long  end_count = start_count + 3* (1 << 20);

		do {
			last_count = flop_count;
			fem_->pstate_->SimulationBody ();
		} while (last_count != flop_count
			&& flop_count < end_count);

		if (max_mflops)
		{
			fem_->pstate_->FlopStats ();
		}
	}
	if (smooth_b_)
		ComputeNormals ();

	if (stereo_direction_)
	{
		do {
			glDrawBuffer ((stereo_direction_ == 1) ? GL_BACK_LEFT : GL_BACK_RIGHT); 
			SetCamera (stereo_direction_);
			DrawFrame();
		} while ((stereo_direction_ *= -1) != 1);
	}
	else
		DrawFrame();
}  

void FEMDrawer::DrawFrame()
{
	global_frame_count++;

	glClear (GL_COLOR_BUFFER_BIT);
	glClear (GL_DEPTH_BUFFER_BIT);

	fem_->Visualize (this);
	DrawScalpel ();
	DrawSelector( );
	DrawFeature ();

#if 0
	OpenglStuff (fem_, this);
#endif


	glFlush();
}


void FEMDrawer::SetLighting ()
{
	if (lighting_b_)
	{
		float m[4][4];
		BuildRotMatrix(m, light_drag_.quat_);

		Matrix3 lm;
		Matrix3::FromOpenglMatrixf (&lm, &m[0][0]);

		Vector3 light_pos = Vector3 (0.8, 1.0, 2.0);
		light_pos = lm * light_pos;
		if (show_back_b_) light_pos = - light_pos;
		float position[] = {light_pos(0),
			light_pos(1),
			light_pos(2),0};


		glClearColor(background[0], background[1], background[2], background[3]);

		// use this to show "back" of model.
		glFrontFace( (show_back_b_) ? GL_CW : GL_CCW);

		glEnable(GL_NORMALIZE);
		glEnable(GL_DEPTH_TEST);
		glLightfv(GL_LIGHT0, GL_POSITION, position);
		glEnable(GL_LIGHTING);
		glEnable(GL_LIGHT0);
		glEnable (GL_CULL_FACE);
		glShadeModel(GL_SMOOTH);      
	}
	else
	{
		glShadeModel(GL_FLAT);            
		glDisable ( GL_CULL_FACE);
		glDisable (GL_LIGHTING);      
	}
}

void FEMDrawer::SetColor(float *c)
{
	//  if (lighting_b_) // solid_b_)
	glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, c);
	//else
	glColor4fv (c);
}

void FEMDrawer::DrawSelector ()
{
	glBegin (GL_LINES);
	Vector3 v = selection_ray_.origin_;
	EnterVertex (v);
	v += selection_ray_.dir_ * intersect_dist_;
	EnterVertex (v);
	glEnd ();

}

void FEMDrawer::SetDepthCue ()
{
	if (fog_b_)
	{
		glHint (GL_FOG_HINT, GL_NICEST);
		glFogf(GL_FOG_START, distance_ - 2);
		glFogf(GL_FOG_END, distance_ + 2);
		glEnable (GL_FOG);
		glEnable (GL_DEPTH_TEST);
		glFogi (GL_FOG_MODE, GL_LINEAR);

		float fog_color[] =  {0.0, 0., 0., 0.0};
		glFogfv(GL_FOG_COLOR, fog_color);
	}
	else
		glDisable (GL_FOG);


	glFlush ();
}


FEMDrawer::~FEMDrawer ()
{
}


void FEMDrawer::RotateDrag (Quaternion * current,
						 int sx, int sy,
						 int beginx, int beginy,
						 int ex, int ey)
{
	/* drag in progress, simulate Trackball */
	float spin_quat[4];
	Trackball(spin_quat,
		(2.0*beginx - sx) / sx,
		(sy - 2.0*beginy) / sy,
		(2.0*ex - sx) / sx,
		(sy - 2.0*ey) / sy);

	AddQuats (spin_quat, *current, *current);

	if(current == &view_drag_.quat_)
		SetCamera ();
	else if(current == &light_drag_.quat_)
		SetLighting ();
	else if (current == &force_drag_.quat_)
		SetSelector ();
	else if (current == &cut_drag_.quat_)
		/*
		only set the sweep (and show the result, don't actually do anything.)

		don't do anything
		*/
		this_sweep_ = last_sweep_.NextSweep (PendingScalpelVelocity ());
}


void FEMDrawer::TranslateDrag (Vector3 *current,
							int sx, int sy,
							int beginx, int beginy,
							int ex, int ey)
{

	Vector3 add = Vector3(  (Real (ex) - beginx ) / sx,
		(beginy - Real (ey) ) / sy ,
		0.0);

	*current += add;

	if (current == &view_drag_.translate_)
	{

		SetCamera ();
	}
	else if (current == &force_drag_.translate_)
	{
		AdjustForce (force_factor_*add); 
	}
}

void FEMDrawer::ScaleDrag(Vector3 *current,
					   int sx, int sy,
					   int beginx, int beginy,
					   int ex, int ey)
{
	Vector3 add = Vector3(  (Real (ex) - beginx ) / sx,
		(beginy - Real (ey) ) / sy ,
		0.0);

	*current += add;

	if (current == &view_drag_.scale_)
	{
		// this is unintuitive.

		//      view_drag_.translate_(2) = 2 * view_drag_.scale_(1);

		SetFrustrum ();
	}
	else if (current == &force_drag_.scale_)
	{
		Vector3 perp (0,0,add(0));
		AdjustForce (force_factor_ *perp); 
	}

	else if (current == &cut_drag_.scale_)
	{
		SetScalpel();
	}
}

void FEMDrawer::SetScalpel ()
{
	this_sweep_ = last_sweep_.NextSweep (PendingScalpelVelocity ());
	if (cut_performer_)
		cut_performer_->SetNextSweep (this_sweep_);
}

void FEMDrawer::ShowCut ()
{
	if (cut_performer_)
		cut_performer_->TryCut ();
}

Vector3 FEMDrawer::PendingScalpelVelocity ()
{
	Vector3 n = last_sweep_.Normal();
	Vector3 d = last_sweep_.points_[2] - last_sweep_.points_[0];

	Vector3 v1 = Vector3::Cross (n, d);
	v1 /=  v1.Length();
	d /= d.Length ();
	Matrix3 to_scalpel;
	to_scalpel.SetVectors (n, v1, d);



	float om[4][4];
	BuildRotMatrix (om, cut_drag_.quat_);
	Matrix3 m;
	Matrix3::FromOpenglMatrixf (&m, &om[0][0]);

	Vector3 vel = to_scalpel * m *  Vector3(0,1,0); 
	vel *= pow (2, cut_drag_.scale_(0)) * GetNumberSetting ("scalpel-velocity");


	return vel;
}

void FEMDrawer::SetSelector ()
{
	Ray & r=selection_ray_;
	r.origin_ = Vector3(0, 0, 2.0);
	r.dir_ =  Vector3(0, 0, -1.0);

	Matrix3 m;
	float om[4][4];

	BuildRotMatrix(om, view_drag_.quat_ );
	Matrix3::FromOpenglMatrixf (&m, &om[0][0]);

	Matrix3 b;
	Matrix3::InvertTo (b, m);

	BuildRotMatrix (om, force_drag_.quat_);
	Matrix3::FromOpenglMatrixf (&m, &om[0][0]);

	m = b * m;
	r.dir_ =   m*r.dir_;
	r.origin_ = m * r.origin_;

	TriIntersection ti = IntersectTriangles (r, fem_, true);

	SetMarked (ti.victim_,0);

	if (ti.victim_)
		intersect_dist_ = ti.distance_;
	else
		/*
		try to avoid putting infinity into the X-server.
		*/
		intersect_dist_ = 1e8;

}

void FEMDrawer::NextCut ()
{
	if (!cut_performer_)
	{
		cut_performer_ = new CutPerformer (fem_);

		last_sweep_.SetInterestingDefault (fem_);

		SetScalpel ();
	}


	cut_performer_->RevertSweepTo (last_sweep_);
	this_sweep_ = last_sweep_.NextSweep (PendingScalpelVelocity ());
	cut_performer_->SetNextSweep (this_sweep_);

	cut_performer_->DoCut ();
	UpdateFaces ();

	Trackball (cut_drag_.quat_,0,0,0,0);

	last_sweep_ = this_sweep_;
	this_sweep_ = last_sweep_.NextSweep (PendingScalpelVelocity ());
	cut_performer_->SetNextSweep (this_sweep_);
}

VOID FEMDrawer::StopCut ()
{
	delete cut_performer_;
	cut_performer_ = 0;
}


void FEMDrawer::Fix ()
{
	fem_->FixObject (marked_, true);
}


void FEMDrawer::Unfix ()
{
	fem_->FixObject (marked_, false);
}

void FEMDrawer::PrintMarked ()
{
	if (marked_)
		cout << marked_->GetSet () << endl;
}

void FEMDrawer::ToggleFog()
{
	fog_b_ = !fog_b_;
	cout << "fog is set to " << fog_b_ << endl;
	SetDepthCue();
}

void FEMDrawer::ToggleEdge()
{
	edge_b_ = !edge_b_;
	cout << "edge is set to " << edge_b_ << endl;
}

void FEMDrawer::ToggleInside()
{
	inside_b_ = !inside_b_;
	cout << "inside is set to " << inside_b_ << endl;
}

void FEMDrawer::ToggleShowBack()
{
	show_back_b_ = !show_back_b_;
	cout << "show_back is set to " << show_back_b_ << endl;
	SetLighting();
}

void FEMDrawer::ToggleLighting()
{
	lighting_b_ = !lighting_b_;
	cout << "lighting is set to " << lighting_b_ << endl;
	SetLighting();
}

void FEMDrawer::ToggleForceDisplay()
{
	force_display_b_ = !force_display_b_;
	cout << "force_display is set to " << force_display_b_ << endl;
}

void FEMDrawer::ToggleTetQuality()
{
	tet_quality_b_ = !tet_quality_b_;
	cout << "tet_quality is set to " << tet_quality_b_ << endl;
}


void FEMDrawer::ToggleSmooth()
{
	smooth_b_ = !smooth_b_;
	cout << "smooth is set to " << smooth_b_ << endl;
}

void FEMDrawer::ToggleNormal()
{
	normal_b_ = !normal_b_;
	cout << "normal is set to " << normal_b_ << endl;
}

void FEMDrawer::ToggleRelaxation()
{
	relaxation_b_ = !relaxation_b_;
	cout << "relaxation is set to " << relaxation_b_ << endl;
}

void FEMDrawer::ToggleSurfaceSelector()
{
	surface_selector_b_ = !surface_selector_b_;
	cout << "surface_selector is set to " << surface_selector_b_ << endl;
}

void FEMDrawer::ToggleSolid()
{
	solid_b_ = !solid_b_;
	cout << "solid is set to " << solid_b_ << endl;
}

void FEMDrawer::ToggleOriginal()
{
	original_b_ = !original_b_;
	cout << "original is set to " << original_b_ << endl;
}

void FEMDrawer::Zoom (Real dx)
{
	distance_ -= dx;
	cout << "distance_ " << distance_ << endl;
	SetCamera ();
	SetDepthCue ();
}

char * FEMDrawer::Stats()
{
	static char s[4024];
	static char t[1024];

	if (marked_)
	{
		sprintf (t, "Marked obj %d\n",  marked_->Number ());
		strcat (s, t);
	}

	if (marked_node_)
	{
		sprintf (t, "Marked node %d\n" , marked_node_->Number () );
		strcat (s, t);
	}

	strcat (s,  FEMStatistics (fem_));
	sprintf (t, "Surface triangles %d\n" , face_arr_.Size ());
	strcat (s,t);

	if (cut_performer_)
		strcat (s,  CutPerformerStatistics ());

	sprintf (t, "Edges: %d\n",  edge_array_.Size ());
	strcat (s,t);

	extern char * CollapseStatistics ();
	extern char * EdgeCollapseStatistics ();

	sprintf (t, "degeneracies: %s\n " ,    CollapseStatistics ());
	strcat (s,t);
	sprintf (t, "edge collapse: %s\n ",     EdgeCollapseStatistics ());
	strcat (s,t);
	return s;
}

void FEMDrawer::UpdateFaces ()
{
	face_arr_.Clear (); 
	std::set<Triangle*>::const_iterator i (fem_->OuterFacesBegin ());
	std::set<Triangle*>::const_iterator e (fem_->OuterFacesEnd ());
	for (; i != e; i++)
	{
		face_arr_.Push ((*i));
		assert ((*i)->Valid ());
	}

	edge_array_ = fem_->GetEdgeArray ();
}

void FEMDrawer::ComputeNormals ()
{
	surface_normals_.SetSize (fem_->NodeCount ());
	vertex_counts_.SetSize (fem_->NodeCount ());

	for (int i = surface_normals_.Size (); i--;)
	{
		surface_normals_[i].Set (0,0,0);
		vertex_counts_[i] =0;
	}
	for (int  i= face_arr_.Size (); i--;)
	{
		Vector3 n = face_arr_[i]->Normal (true);

		for (int j = 3; j--;)
		{
			int k = face_arr_[i]->GetNode (j)->Number ();

			surface_normals_[k] += n;
			vertex_counts_[k] ++;
		}
	}

	for (int i = surface_normals_.Size (); i--;)
	{
		surface_normals_[i] *= 1.0 / vertex_counts_[i];
	}
}


#endif // FEMDRAWER_H
